﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.Collections.Generic;

namespace SharpGeo
{
    /// <summary>
    ///Represents an immutable collection of coordinates
    /// </summary>
    sealed public class ReadOnlyCoordinateCollection :
        IReadOnlyCoordinateCollection
    {
        #region Constructors

        /// <summary>
        /// Constructs a new ReadOnlyCoordinateCollection from an array of double ordinates
        /// </summary>
        /// <param name="ordinates">A double array packed with ordinate values</param>
        /// <param name="numberOfAxes">The number of axes</param>
        public ReadOnlyCoordinateCollection(double[] ordinates, int numberOfAxes)
        {
            if (ordinates == null || numberOfAxes < 1 || ordinates.Length == 0)
            {
                _count = 0;
                _numberOfAxes = 0;
                _ordinates = new double[0];
            }
            else
            {
                if (ordinates.Length % numberOfAxes != 0)
                {
                    throw new ArgumentException(
                        Properties.Resources.ReadOnlyCoordinateCollectionOrdinateDivisible,
                        "ordinates");
                }

                _ordinates = (double[])ordinates.Clone();
                _numberOfAxes = numberOfAxes;
                _count = _ordinates.Length / _numberOfAxes;
            }
        }

        /// <summary>
        /// Constructs a new ReadOnlyCoordinateCollection from an array of ICoordinate
        /// </summary>
        /// <param name="coordinates">The ICoordinate array to construct this ReadOnlyCoordinateCollection from</param>
        public ReadOnlyCoordinateCollection(ICoordinate[] coordinates)
        {
            if (coordinates == null || coordinates.Length == 0)
            {
                _ordinates = new double[0];
                _numberOfAxes = 0;
                _count = 0;
            }
            else
            {
                _ordinates = new double[coordinates.Length * coordinates[0].NumberOfAxes];
                _count = coordinates.Length;
                _numberOfAxes = coordinates[0].NumberOfAxes;

                for (int i = 0, index = 0; i < coordinates.Length; i++, index += _numberOfAxes)
                {
                    if (coordinates[i].NumberOfAxes != _numberOfAxes)
                    {
                        throw new ArgumentException(
                            Properties.Resources.ReadOnlyCoordinateCollectionCoordinateSameAxis,
                            "coordinates");
                    }

                    for (int j = 0; j < _numberOfAxes; j++)
                    {
                        _ordinates[index + j] = coordinates[i][j];
                    }
                }
            }
        }

        #endregion Constructors

        #region Fields

        private int _count;
        private int _numberOfAxes;
        private double[] _ordinates;
        #endregion Fields

        #region Properties/Fields

        /// <summary>
        /// Gets number of ICoordinates stored in this collection
        /// </summary>
        public int Count { get { return _count; } }

        /// <summary>
        /// Gets the number of axes of the ICoordinates stored in this collection
        /// </summary>
        public int NumberOfAxes { get { return _numberOfAxes; } }

        #endregion Properties/Fields

        #region Methods

        /// <summary>
        /// Calculates the Envelope of this ReadOnlyCoordinateCollection
        /// </summary>
        /// <returns>The calculated envelope</returns>
        public Envelope CalculateEnvelope()
        {
            if (_numberOfAxes < 2 || _count < 1)
            {
                return Envelope.Empty;
            }

            double minX, minY, maxX, maxY;

            minX = maxX = _ordinates[0];
            minY = maxY = _ordinates[1];

            for (int i = 1, index = _numberOfAxes; i < _count; i++, index += _numberOfAxes)
            {
                var x = _ordinates[index];
                var y = _ordinates[index + 1];

                if (x > maxX)
                    maxX = x;

                if (x < minX)
                    minX = x;

                if (y > maxY)
                    maxY = y;

                if (y < minY)
                    minY = y;
            }

            return new Envelope(minX, minY, maxX, maxY);
        }

        /// <summary>
        /// Returns an ICoordinate saved at a given index
        /// </summary>
        /// <param name="index">The index of the ICoordinate</param>
        /// <returns>Returns an ICoordinate saved at the passed index</returns>
        public ICoordinate GetValue(int index)
        {
            if (_count == 0 || index > (int.MaxValue - 1))
                return null;

            if (((index + 1) * _numberOfAxes) <= _ordinates.Length)
                return new CoordinateWrapper(this, index * NumberOfAxes);

            return null;
        }

        /// <summary>
        /// Gets the enumerator
        /// </summary>
        /// <returns>The enumerator</returns>
        IEnumerator<ICoordinate> IEnumerable<ICoordinate>.GetEnumerator()
        {
            for (int i = 0, index = 0; i < _count; i++, index += _numberOfAxes)
            {
                yield return new CoordinateWrapper(this, index);
            }
        }

        /// <summary>
        /// Determines if the linear ring is wound in a clockwise manner
        /// </summary>
        /// <returns>True if the linear ring is clockwise</returns>
        public bool IsClockwise()
        {
            var sum = 0d;

            for (int i = 0, index = 0; i < _count; i++, index += _numberOfAxes)
            {
                var x1 = _ordinates[index];
                var y1 = _ordinates[index + 1];

                var x2 = i < (_count - 1) ? _ordinates[index + 2] : _ordinates[0];
                var y2 = i < (_count - 1) ? _ordinates[index + 3] : _ordinates[1];

                sum += (x2 - x1) * (y2 + y1);
            }
            return sum > 0d;
        }
        /// <summary>
        /// Gets the enumerator
        /// </summary>
        /// <returns>The enumerator</returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<ICoordinate>)this).GetEnumerator();
        }

        #endregion Methods

        #region CoordinateWrapper Class

        /// <summary>
        /// A nested helper that wraps an entry in the packed double array with a polymorphic wrapper
        /// </summary>
        private class CoordinateWrapper :
            ICoordinate,
            IGeographicCoordinate,
            IGeocentricCoordinate,
            IScreenCoordinate
        {
            private int _index;

            private ReadOnlyCoordinateCollection _parent;

            public CoordinateWrapper(ReadOnlyCoordinateCollection parent, int index)
            {
                _parent = parent;
                _index = index;
            }
            public double Latitude
            {
                get { return this[1]; }
            }

            public double Longitude
            {
                get { return this[0]; }
            }

            public int NumberOfAxes { get { return _parent.NumberOfAxes; } }

            public double X
            {
                get { return this[0]; }
            }

            public double Y
            {
                get { return this[1]; }
            }

            public double Z
            {
                get { return this[2]; }
            }

            public double this[int axis]
            {
                get
                {
                    if (axis > _parent.NumberOfAxes)
                    {
                        return double.NaN;
                    }

                    return _parent._ordinates[_index + axis];
                }
            }
        }

        #endregion CoordinateWrapper Class
    }
}